@@ -2,16 +2,19 @@ package ghmcp
2
2
3
3
import (
4
4
"context"
5
+ "errors"
5
6
"fmt"
6
7
"io"
7
8
"log"
8
9
"net/http"
9
10
"net/url"
10
11
"os"
11
12
"os/signal"
13
+ "strconv"
12
14
"strings"
13
15
"syscall"
14
16
17
+ "github.com/bradleyfalzon/ghinstallation/v2"
15
18
"github.com/github/github-mcp-server/pkg/github"
16
19
mcplog "github.com/github/github-mcp-server/pkg/log"
17
20
"github.com/github/github-mcp-server/pkg/translations"
@@ -20,17 +23,29 @@ import (
20
23
"github.com/mark3labs/mcp-go/server"
21
24
"github.com/shurcooL/githubv4"
22
25
"github.com/sirupsen/logrus"
26
+ "github.com/spf13/viper"
23
27
)
24
28
29
+ // AuthConfig represents authentication configuration
30
+ type AuthConfig struct {
31
+ // Personal Access Token authentication
32
+ Token string
33
+
34
+ // GitHub App authentication
35
+ AppID string
36
+ InstallationID string
37
+ PrivateKeyPEM string
38
+ }
39
+
25
40
type MCPServerConfig struct {
26
41
// Version of the server
27
42
Version string
28
43
29
44
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
30
45
Host string
31
46
32
- // GitHub Token to authenticate with the GitHub API
33
- Token string
47
+ // Authentication configuration
48
+ Auth AuthConfig
34
49
35
50
// EnabledToolsets is a list of toolsets to enable
36
51
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
@@ -47,37 +62,165 @@ type MCPServerConfig struct {
47
62
Translator translations.TranslationHelperFunc
48
63
}
49
64
65
+ // authMethod represents the authentication method being used
66
+ type authMethod int
67
+
68
+ const (
69
+ authToken authMethod = iota
70
+ authGitHubApp
71
+ )
72
+
73
+ // getAuthMethod determines which authentication method to use based on the config
74
+ func (cfg * MCPServerConfig ) getAuthMethod () (authMethod , error ) {
75
+ hasToken := cfg .Auth .Token != ""
76
+ hasApp := cfg .Auth .AppID != "" && cfg .Auth .InstallationID != "" && 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
+ transport , err := ghinstallation .New (
106
+ http .DefaultTransport ,
107
+ appID ,
108
+ installationID ,
109
+ []byte (cfg .Auth .PrivateKeyPEM ),
110
+ )
111
+ if err != nil {
112
+ return nil , fmt .Errorf ("failed to create GitHub App transport: %w" , err )
113
+ }
114
+
115
+ return transport , nil
116
+ }
117
+
118
+ // BuildAuthConfig creates an AuthConfig based on environment variables and flags
119
+ func BuildAuthConfig () (AuthConfig , error ) {
120
+ var authConfig AuthConfig
121
+
122
+ // Check for Personal Access Token
123
+ token := viper .GetString ("personal_access_token" )
124
+
125
+ // Check for GitHub App credentials
126
+ appID := viper .GetString ("app_id" )
127
+ installationID := viper .GetString ("installation_id" )
128
+ privateKeyPEM := viper .GetString ("private_key_pem" )
129
+
130
+ // Determine authentication method
131
+ hasToken := token != ""
132
+ hasApp := appID != "" && installationID != "" && privateKeyPEM != ""
133
+
134
+ if ! hasToken && ! hasApp {
135
+ return authConfig , errors .New ("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN or GitHub App credentials (GITHUB_APP_ID, GITHUB_INSTALLATION_ID, and GITHUB_PRIVATE_KEY_PEM)" )
136
+ }
137
+
138
+ if hasToken && hasApp {
139
+ return authConfig , errors .New ("cannot specify both personal access token and GitHub App authentication" )
140
+ }
141
+
142
+ if hasToken {
143
+ authConfig .Token = token
144
+ } else {
145
+ authConfig .AppID = appID
146
+ authConfig .InstallationID = installationID
147
+ authConfig .PrivateKeyPEM = privateKeyPEM
148
+ }
149
+
150
+ return authConfig , nil
151
+ }
152
+
50
153
func NewMCPServer (cfg MCPServerConfig ) (* server.MCPServer , error ) {
51
154
apiHost , err := parseAPIHost (cfg .Host )
52
155
if err != nil {
53
156
return nil , fmt .Errorf ("failed to parse API host: %w" , err )
54
157
}
55
158
159
+ authMethod , err := cfg .getAuthMethod ()
160
+ if err != nil {
161
+ return nil , fmt .Errorf ("authentication configuration error: %w" , err )
162
+ }
163
+
164
+ // Create HTTP client based on authentication method
165
+ var httpClient * http.Client
166
+ var userAgent string
167
+
168
+ switch authMethod {
169
+ case authToken :
170
+ // Use token-based authentication
171
+ httpClient = & http.Client {
172
+ Transport : & bearerAuthTransport {
173
+ transport : http .DefaultTransport ,
174
+ token : cfg .Auth .Token ,
175
+ },
176
+ }
177
+ userAgent = fmt .Sprintf ("github-mcp-server/%s" , cfg .Version )
178
+
179
+ case authGitHubApp :
180
+ // Use GitHub App authentication
181
+ transport , err := cfg .createGitHubAppTransport ()
182
+ if err != nil {
183
+ return nil , err
184
+ }
185
+
186
+ httpClient = & http.Client {Transport : transport }
187
+ userAgent = fmt .Sprintf ("github-mcp-server/%s (GitHub App)" , cfg .Version )
188
+ }
189
+
56
190
// Construct our REST client
57
- restClient := gogithub .NewClient (nil ).WithAuthToken (cfg .Token )
58
- restClient .UserAgent = fmt .Sprintf ("github-mcp-server/%s" , cfg .Version )
191
+ var restClient * gogithub.Client
192
+ if authMethod == authToken {
193
+ restClient = gogithub .NewClient (nil ).WithAuthToken (cfg .Auth .Token )
194
+ } else {
195
+ restClient = gogithub .NewClient (httpClient )
196
+ }
197
+
198
+ restClient .UserAgent = userAgent
59
199
restClient .BaseURL = apiHost .baseRESTURL
60
200
restClient .UploadURL = apiHost .uploadURL
61
201
62
202
// 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
203
+ gqlHTTPClient := & http.Client {Transport : httpClient .Transport }
71
204
gqlClient := githubv4 .NewEnterpriseClient (apiHost .graphqlURL .String (), gqlHTTPClient )
72
205
73
206
// When a client send an initialize request, update the user agent to include the client info.
74
207
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
- )
208
+ var userAgent string
209
+ if authMethod == authGitHubApp {
210
+ userAgent = fmt .Sprintf (
211
+ "github-mcp-server/%s (%s/%s) (GitHub App)" ,
212
+ cfg .Version ,
213
+ message .Params .ClientInfo .Name ,
214
+ message .Params .ClientInfo .Version ,
215
+ )
216
+ } else {
217
+ userAgent = fmt .Sprintf (
218
+ "github-mcp-server/%s (%s/%s)" ,
219
+ cfg .Version ,
220
+ message .Params .ClientInfo .Name ,
221
+ message .Params .ClientInfo .Version ,
222
+ )
223
+ }
81
224
82
225
restClient .UserAgent = userAgent
83
226
@@ -146,8 +289,8 @@ type StdioServerConfig struct {
146
289
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
147
290
Host string
148
291
149
- // GitHub Token to authenticate with the GitHub API
150
- Token string
292
+ // Authentication configuration
293
+ Auth AuthConfig
151
294
152
295
// EnabledToolsets is a list of toolsets to enable
153
296
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
@@ -182,7 +325,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
182
325
ghServer , err := NewMCPServer (MCPServerConfig {
183
326
Version : cfg .Version ,
184
327
Host : cfg .Host ,
185
- Token : cfg .Token ,
328
+ Auth : cfg .Auth ,
186
329
EnabledToolsets : cfg .EnabledToolsets ,
187
330
DynamicToolsets : cfg .DynamicToolsets ,
188
331
ReadOnly : cfg .ReadOnly ,
0 commit comments