@@ -9,9 +9,11 @@ import (
9
9
"net/url"
10
10
"os"
11
11
"os/signal"
12
+ "strconv"
12
13
"strings"
13
14
"syscall"
14
15
16
+ "github.com/bradleyfalzon/ghinstallation/v2"
15
17
"github.com/github/github-mcp-server/pkg/github"
16
18
mcplog "github.com/github/github-mcp-server/pkg/log"
17
19
"github.com/github/github-mcp-server/pkg/translations"
@@ -22,15 +24,27 @@ import (
22
24
"github.com/sirupsen/logrus"
23
25
)
24
26
27
+ // AuthConfig represents authentication configuration
28
+ type AuthConfig struct {
29
+ // Personal Access Token authentication
30
+ Token string
31
+
32
+ // GitHub App authentication
33
+ AppID string
34
+ InstallationID string
35
+ PrivateKeyPath string
36
+ PrivateKeyPEM string // Alternative to PrivateKeyPath - raw PEM content
37
+ }
38
+
25
39
type MCPServerConfig struct {
26
40
// Version of the server
27
41
Version string
28
42
29
43
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
30
44
Host string
31
45
32
- // GitHub Token to authenticate with the GitHub API
33
- Token string
46
+ // Authentication configuration
47
+ Auth AuthConfig
34
48
35
49
// EnabledToolsets is a list of toolsets to enable
36
50
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
@@ -47,43 +61,151 @@ type MCPServerConfig struct {
47
61
Translator translations.TranslationHelperFunc
48
62
}
49
63
64
+ // authMethod represents the authentication method being used
65
+ type authMethod int
66
+
67
+ const (
68
+ authToken authMethod = iota
69
+ authGitHubApp
70
+ )
71
+
72
+ // getAuthMethod determines which authentication method to use based on the config
73
+ func (cfg * MCPServerConfig ) getAuthMethod () (authMethod , error ) {
74
+ hasToken := cfg .Auth .Token != ""
75
+ hasApp := cfg .Auth .AppID != "" && cfg .Auth .InstallationID != "" &&
76
+ (cfg .Auth .PrivateKeyPath != "" || cfg .Auth .PrivateKeyPEM != "" )
77
+
78
+ if hasToken && hasApp {
79
+ return 0 , fmt .Errorf ("cannot specify both token and GitHub App authentication" )
80
+ }
81
+
82
+ if ! hasToken && ! hasApp {
83
+ return 0 , fmt .Errorf ("must specify either token or GitHub App authentication" )
84
+ }
85
+
86
+ if hasToken {
87
+ return authToken , nil
88
+ }
89
+
90
+ return authGitHubApp , nil
91
+ }
92
+
93
+ // createGitHubAppTransport creates an authenticated transport for GitHub App
94
+ func (cfg * MCPServerConfig ) createGitHubAppTransport () (http.RoundTripper , error ) {
95
+ appID , err := strconv .ParseInt (cfg .Auth .AppID , 10 , 64 )
96
+ if err != nil {
97
+ return nil , fmt .Errorf ("invalid app ID: %w" , err )
98
+ }
99
+
100
+ installationID , err := strconv .ParseInt (cfg .Auth .InstallationID , 10 , 64 )
101
+ if err != nil {
102
+ return nil , fmt .Errorf ("invalid installation ID: %w" , err )
103
+ }
104
+
105
+ var transport * ghinstallation.Transport
106
+
107
+ if cfg .Auth .PrivateKeyPEM != "" {
108
+ // Use PEM content directly
109
+ transport , err = ghinstallation .New (
110
+ http .DefaultTransport ,
111
+ appID ,
112
+ installationID ,
113
+ []byte (cfg .Auth .PrivateKeyPEM ),
114
+ )
115
+ } else {
116
+ // Use private key file
117
+ transport , err = ghinstallation .NewKeyFromFile (
118
+ http .DefaultTransport ,
119
+ appID ,
120
+ installationID ,
121
+ cfg .Auth .PrivateKeyPath ,
122
+ )
123
+ }
124
+
125
+ if err != nil {
126
+ return nil , fmt .Errorf ("failed to create GitHub App transport: %w" , err )
127
+ }
128
+
129
+ return transport , nil
130
+ }
131
+
50
132
func NewMCPServer (cfg MCPServerConfig ) (* server.MCPServer , error ) {
51
133
apiHost , err := parseAPIHost (cfg .Host )
52
134
if err != nil {
53
135
return nil , fmt .Errorf ("failed to parse API host: %w" , err )
54
136
}
55
137
138
+ authMethod , err := cfg .getAuthMethod ()
139
+ if err != nil {
140
+ return nil , fmt .Errorf ("authentication configuration error: %w" , err )
141
+ }
142
+
143
+ // Create HTTP client based on authentication method
144
+ var httpClient * http.Client
145
+ var userAgent string
146
+
147
+ switch authMethod {
148
+ case authToken :
149
+ // Use token-based authentication (existing behavior)
150
+ httpClient = & http.Client {
151
+ Transport : & bearerAuthTransport {
152
+ transport : http .DefaultTransport ,
153
+ token : cfg .Auth .Token ,
154
+ },
155
+ }
156
+ userAgent = fmt .Sprintf ("github-mcp-server/%s" , cfg .Version )
157
+
158
+ case authGitHubApp :
159
+ // Use GitHub App authentication
160
+ transport , err := cfg .createGitHubAppTransport ()
161
+ if err != nil {
162
+ return nil , err
163
+ }
164
+
165
+ httpClient = & http.Client {Transport : transport }
166
+ userAgent = fmt .Sprintf ("github-mcp-server/%s (GitHub App)" , cfg .Version )
167
+ }
168
+
56
169
// Construct our REST client
57
- restClient := gogithub .NewClient (nil ).WithAuthToken (cfg .Token )
58
- restClient .UserAgent = fmt .Sprintf ("github-mcp-server/%s" , cfg .Version )
170
+ var restClient * gogithub.Client
171
+ if authMethod == authToken {
172
+ restClient = gogithub .NewClient (nil ).WithAuthToken (cfg .Auth .Token )
173
+ } else {
174
+ restClient = gogithub .NewClient (httpClient )
175
+ }
176
+
177
+ restClient .UserAgent = userAgent
59
178
restClient .BaseURL = apiHost .baseRESTURL
60
179
restClient .UploadURL = apiHost .uploadURL
61
180
62
181
// 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
182
+ gqlHTTPClient := & http.Client {Transport : httpClient .Transport }
71
183
gqlClient := githubv4 .NewEnterpriseClient (apiHost .graphqlURL .String (), gqlHTTPClient )
72
184
73
185
// When a client send an initialize request, update the user agent to include the client info.
74
186
beforeInit := func (_ context.Context , _ any , message * mcp.InitializeRequest ) {
75
- userAgent := fmt .Sprintf (
76
- "github-mcp-server/%s (%s/%s)" ,
77
- cfg .Version ,
78
- message .Params .ClientInfo .Name ,
79
- message .Params .ClientInfo .Version ,
80
- )
187
+ var newUserAgent string
188
+ if authMethod == authGitHubApp {
189
+ newUserAgent = fmt .Sprintf (
190
+ "github-mcp-server/%s (%s/%s) (GitHub App)" ,
191
+ cfg .Version ,
192
+ message .Params .ClientInfo .Name ,
193
+ message .Params .ClientInfo .Version ,
194
+ )
195
+ } else {
196
+ newUserAgent = fmt .Sprintf (
197
+ "github-mcp-server/%s (%s/%s)" ,
198
+ cfg .Version ,
199
+ message .Params .ClientInfo .Name ,
200
+ message .Params .ClientInfo .Version ,
201
+ )
202
+ }
81
203
82
- restClient .UserAgent = userAgent
204
+ restClient .UserAgent = newUserAgent
83
205
84
206
gqlHTTPClient .Transport = & userAgentTransport {
85
207
transport : gqlHTTPClient .Transport ,
86
- agent : userAgent ,
208
+ agent : newUserAgent ,
87
209
}
88
210
}
89
211
@@ -146,8 +268,8 @@ type StdioServerConfig struct {
146
268
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
147
269
Host string
148
270
149
- // GitHub Token to authenticate with the GitHub API
150
- Token string
271
+ // Authentication configuration
272
+ Auth AuthConfig
151
273
152
274
// EnabledToolsets is a list of toolsets to enable
153
275
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
@@ -182,7 +304,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
182
304
ghServer , err := NewMCPServer (MCPServerConfig {
183
305
Version : cfg .Version ,
184
306
Host : cfg .Host ,
185
- Token : cfg .Token ,
307
+ Auth : cfg .Auth ,
186
308
EnabledToolsets : cfg .EnabledToolsets ,
187
309
DynamicToolsets : cfg .DynamicToolsets ,
188
310
ReadOnly : cfg .ReadOnly ,
0 commit comments