Skip to content

Commit b33410a

Browse files
committed
add github app based auth
1 parent 023f59d commit b33410a

File tree

4 files changed

+210
-28
lines changed

4 files changed

+210
-28
lines changed

cmd/github-mcp-server/main.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ var (
2929
Short: "Start stdio server",
3030
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
3131
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")
32+
// Validate authentication configuration
33+
authConfig, err := buildAuthConfig()
34+
if err != nil {
35+
return err
3536
}
3637

3738
// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
@@ -46,7 +47,7 @@ var (
4647
stdioServerConfig := ghmcp.StdioServerConfig{
4748
Version: version,
4849
Host: viper.GetString("host"),
49-
Token: token,
50+
Auth: authConfig,
5051
EnabledToolsets: enabledToolsets,
5152
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
5253
ReadOnly: viper.GetBool("read-only"),
@@ -60,6 +61,43 @@ var (
6061
}
6162
)
6263

64+
// buildAuthConfig creates an AuthConfig based on environment variables and flags
65+
func buildAuthConfig() (ghmcp.AuthConfig, error) {
66+
var authConfig ghmcp.AuthConfig
67+
68+
// Check for Personal Access Token
69+
token := viper.GetString("personal_access_token")
70+
71+
// Check for GitHub App credentials
72+
appID := viper.GetString("app_id")
73+
installationID := viper.GetString("installation_id")
74+
privateKeyPath := viper.GetString("private_key_path")
75+
privateKeyPEM := viper.GetString("private_key_pem")
76+
77+
// Determine authentication method
78+
hasToken := token != ""
79+
hasApp := appID != "" && installationID != "" && (privateKeyPath != "" || privateKeyPEM != "")
80+
81+
if !hasToken && !hasApp {
82+
return authConfig, errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN or GitHub App credentials (GITHUB_APP_ID, GITHUB_INSTALLATION_ID, and either GITHUB_PRIVATE_KEY_PATH or GITHUB_PRIVATE_KEY_PEM)")
83+
}
84+
85+
if hasToken && hasApp {
86+
return authConfig, errors.New("cannot specify both personal access token and GitHub App authentication")
87+
}
88+
89+
if hasToken {
90+
authConfig.Token = token
91+
} else {
92+
authConfig.AppID = appID
93+
authConfig.InstallationID = installationID
94+
authConfig.PrivateKeyPath = privateKeyPath
95+
authConfig.PrivateKeyPEM = privateKeyPEM
96+
}
97+
98+
return authConfig, nil
99+
}
100+
63101
func init() {
64102
cobra.OnInitialize(initConfig)
65103

@@ -74,7 +112,13 @@ func init() {
74112
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
75113
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
76114

77-
// Bind flag to viper
115+
// Add GitHub App authentication flags
116+
rootCmd.PersistentFlags().String("app-id", "", "GitHub App ID")
117+
rootCmd.PersistentFlags().String("installation-id", "", "GitHub App Installation ID")
118+
rootCmd.PersistentFlags().String("private-key-path", "", "Path to GitHub App private key file")
119+
rootCmd.PersistentFlags().String("private-key-pem", "", "GitHub App private key PEM content")
120+
121+
// Bind flags to viper
78122
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
79123
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
80124
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
@@ -83,6 +127,12 @@ func init() {
83127
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
84128
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
85129

130+
// Bind GitHub App flags to viper
131+
_ = viper.BindPFlag("app_id", rootCmd.PersistentFlags().Lookup("app-id"))
132+
_ = viper.BindPFlag("installation_id", rootCmd.PersistentFlags().Lookup("installation-id"))
133+
_ = viper.BindPFlag("private_key_path", rootCmd.PersistentFlags().Lookup("private-key-path"))
134+
_ = viper.BindPFlag("private_key_pem", rootCmd.PersistentFlags().Lookup("private-key-pem"))
135+
86136
// Add subcommands
87137
rootCmd.AddCommand(stdioCmd)
88138
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/github/github-mcp-server
33
go 1.23.7
44

55
require (
6+
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0
67
github.com/google/go-github/v69 v69.2.0
78
github.com/josephburnett/jd v1.9.2
89
github.com/mark3labs/mcp-go v0.30.0
@@ -16,6 +17,8 @@ require (
1617
require (
1718
github.com/go-openapi/jsonpointer v0.19.5 // indirect
1819
github.com/go-openapi/swag v0.21.1 // indirect
20+
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
21+
github.com/google/go-github/v62 v62.0.0 // indirect
1922
github.com/josharian/intern v1.0.0 // indirect
2023
github.com/mailru/easyjson v0.7.7 // indirect
2124
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag=
2+
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M=
13
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
24
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
35
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -15,9 +17,14 @@ github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrK
1517
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
1618
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
1719
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
20+
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
21+
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
1822
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
23+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1924
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2025
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
26+
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
27+
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
2128
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
2229
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
2330
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=

internal/ghmcp/server.go

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"net/url"
1010
"os"
1111
"os/signal"
12+
"strconv"
1213
"strings"
1314
"syscall"
1415

16+
"github.com/bradleyfalzon/ghinstallation/v2"
1517
"github.com/github/github-mcp-server/pkg/github"
1618
mcplog "github.com/github/github-mcp-server/pkg/log"
1719
"github.com/github/github-mcp-server/pkg/translations"
@@ -22,15 +24,27 @@ import (
2224
"github.com/sirupsen/logrus"
2325
)
2426

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+
2539
type MCPServerConfig struct {
2640
// Version of the server
2741
Version string
2842

2943
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
3044
Host string
3145

32-
// GitHub Token to authenticate with the GitHub API
33-
Token string
46+
// Authentication configuration
47+
Auth AuthConfig
3448

3549
// EnabledToolsets is a list of toolsets to enable
3650
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
@@ -47,43 +61,151 @@ type MCPServerConfig struct {
4761
Translator translations.TranslationHelperFunc
4862
}
4963

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+
50132
func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
51133
apiHost, err := parseAPIHost(cfg.Host)
52134
if err != nil {
53135
return nil, fmt.Errorf("failed to parse API host: %w", err)
54136
}
55137

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+
56169
// 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
59178
restClient.BaseURL = apiHost.baseRESTURL
60179
restClient.UploadURL = apiHost.uploadURL
61180

62181
// 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}
71183
gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)
72184

73185
// When a client send an initialize request, update the user agent to include the client info.
74186
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+
}
81203

82-
restClient.UserAgent = userAgent
204+
restClient.UserAgent = newUserAgent
83205

84206
gqlHTTPClient.Transport = &userAgentTransport{
85207
transport: gqlHTTPClient.Transport,
86-
agent: userAgent,
208+
agent: newUserAgent,
87209
}
88210
}
89211

@@ -146,8 +268,8 @@ type StdioServerConfig struct {
146268
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
147269
Host string
148270

149-
// GitHub Token to authenticate with the GitHub API
150-
Token string
271+
// Authentication configuration
272+
Auth AuthConfig
151273

152274
// EnabledToolsets is a list of toolsets to enable
153275
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
@@ -182,7 +304,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
182304
ghServer, err := NewMCPServer(MCPServerConfig{
183305
Version: cfg.Version,
184306
Host: cfg.Host,
185-
Token: cfg.Token,
307+
Auth: cfg.Auth,
186308
EnabledToolsets: cfg.EnabledToolsets,
187309
DynamicToolsets: cfg.DynamicToolsets,
188310
ReadOnly: cfg.ReadOnly,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy