diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index fb716f78d..091819986 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -58,6 +58,53 @@ var ( return ghmcp.RunStdioServer(stdioServerConfig) }, } + + httpCmd = &cobra.Command{ + Use: "http", + Short: "Start HTTP server", + Long: `Start a server that communicates via HTTP using the Streamable-HTTP transport.`, + RunE: func(_ *cobra.Command, _ []string) error { + // Check if we have either a personal access token or GitHub App credentials + token := viper.GetString("personal_access_token") + appID := viper.GetString("app_id") + appPrivateKey := viper.GetString("app_private_key") + enableGitHubAppAuth := viper.GetBool("enable_github_app_auth") + + if token == "" && (!enableGitHubAppAuth || appID == "" || appPrivateKey == "") { + return errors.New("either GITHUB_PERSONAL_ACCESS_TOKEN or GitHub App credentials (GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY) must be set") + } + + // If you're wondering why we're not using viper.GetStringSlice("toolsets"), + // it's because viper doesn't handle comma-separated values correctly for env + // vars when using GetStringSlice. + // https://github.com/spf13/viper/issues/380 + var enabledToolsets []string + if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil { + return fmt.Errorf("failed to unmarshal toolsets: %w", err) + } + + httpServerConfig := ghmcp.HttpServerConfig{ + Version: version, + Host: viper.GetString("host"), + Token: token, + EnabledToolsets: enabledToolsets, + DynamicToolsets: viper.GetBool("dynamic_toolsets"), + ReadOnly: viper.GetBool("read-only"), + ExportTranslations: viper.GetBool("export-translations"), + EnableCommandLogging: viper.GetBool("enable-command-logging"), + LogFilePath: viper.GetString("log-file"), + Address: viper.GetString("http_address"), + MCPPath: viper.GetString("http_mcp_path"), + EnableCORS: viper.GetBool("http_enable_cors"), + AppID: appID, + AppPrivateKey: appPrivateKey, + EnableGitHubAppAuth: enableGitHubAppAuth, + InstallationIDHeader: viper.GetString("installation_id_header"), + } + + return ghmcp.RunHTTPServer(httpServerConfig) + }, + } ) func init() { @@ -74,7 +121,18 @@ func init() { rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file") rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") - // Bind flag to viper + // GitHub App authentication flags + rootCmd.PersistentFlags().String("app-id", "", "GitHub App ID for authentication") + rootCmd.PersistentFlags().String("app-private-key", "", "GitHub App private key for authentication") + rootCmd.PersistentFlags().Bool("enable-github-app-auth", false, "Enable GitHub App authentication via custom headers") + rootCmd.PersistentFlags().String("installation-id-header", "X-GitHub-Installation-ID", "Custom header name to read installation ID from") + + // HTTP server specific flags + httpCmd.Flags().String("http-address", ":8080", "HTTP server address to bind to") + httpCmd.Flags().String("http-mcp-path", "/mcp", "HTTP path for MCP endpoint") + httpCmd.Flags().Bool("http-enable-cors", false, "Enable CORS for cross-origin requests") + + // Bind flags to viper _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets")) _ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets")) _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) @@ -82,9 +140,17 @@ func init() { _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) _ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host")) + _ = viper.BindPFlag("app_id", rootCmd.PersistentFlags().Lookup("app-id")) + _ = viper.BindPFlag("app_private_key", rootCmd.PersistentFlags().Lookup("app-private-key")) + _ = viper.BindPFlag("enable_github_app_auth", rootCmd.PersistentFlags().Lookup("enable-github-app-auth")) + _ = viper.BindPFlag("installation_id_header", rootCmd.PersistentFlags().Lookup("installation-id-header")) + _ = viper.BindPFlag("http_address", httpCmd.Flags().Lookup("http-address")) + _ = viper.BindPFlag("http_mcp_path", httpCmd.Flags().Lookup("http-mcp-path")) + _ = viper.BindPFlag("http_enable_cors", httpCmd.Flags().Lookup("http-enable-cors")) // Add subcommands rootCmd.AddCommand(stdioCmd) + rootCmd.AddCommand(httpCmd) } func initConfig() { diff --git a/go.mod b/go.mod index ab2302ed5..568160cbd 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,9 @@ require ( require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.21.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect + github.com/google/go-github/v69 v69.2.0 // indirect + github.com/jferrl/go-githubauth v1.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect @@ -30,7 +33,7 @@ require ( github.com/google/go-github/v71 v71.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -45,10 +48,10 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.6.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e7f6794a7..e7226d017 100644 --- a/go.sum +++ b/go.sum @@ -15,9 +15,13 @@ github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrK github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= +github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= @@ -28,8 +32,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jferrl/go-githubauth v1.2.0 h1:K138gEpO2e/yBf6OI5Vb7+0xgZZa7N7/su/iAAG0ieU= +github.com/jferrl/go-githubauth v1.2.0/go.mod h1:mglSJcfvt4HSvuzQKYx4vkvi1PtlMj88m2gz660QuC0= github.com/josephburnett/jd v1.9.2 h1:ECJRRFXCCqbtidkAHckHGSZm/JIaAxS1gygHLF8MI5Y= github.com/josephburnett/jd v1.9.2/go.mod h1:bImDr8QXpxMb3SD+w1cDRHp97xP6UwI88xUAuxwDQfM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -98,6 +106,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -105,6 +115,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/ghmcp/http_server.go b/internal/ghmcp/http_server.go new file mode 100644 index 000000000..a5d9ddc05 --- /dev/null +++ b/internal/ghmcp/http_server.go @@ -0,0 +1,229 @@ +package ghmcp + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/server" + "github.com/sirupsen/logrus" +) + +type HttpServerConfig struct { + // Version of the server + Version string + + // GitHub Host to target for API requests (e.g. github.com or github.enterprise.com) + Host string + + // GitHub Token to authenticate with the GitHub API + Token string + + // EnabledToolsets is a list of toolsets to enable + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration + EnabledToolsets []string + + // Whether to enable dynamic toolsets + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery + DynamicToolsets bool + + // ReadOnly indicates if we should only register read-only tools + ReadOnly bool + + // ExportTranslations indicates if we should export translations + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions + ExportTranslations bool + + // EnableCommandLogging indicates if we should log commands + EnableCommandLogging bool + + // Path to the log file if not stderr + LogFilePath string + + // HTTP server configuration + Address string + + // MCP endpoint path (defaults to "/mcp") + MCPPath string + + // Enable CORS for cross-origin requests + EnableCORS bool + + // GITHUB APP ID + AppID string + + // GITHUB APP PRIVATE KEY + AppPrivateKey string + + // Whether to enable GitHub App authentication via headers + EnableGitHubAppAuth bool + + // Custom header name to read installation ID from (defaults to "X-GitHub-Installation-ID") + InstallationIDHeader string +} + +const installationContextKey = "installation_id" + +// RunHTTPServer is not concurrent safe. +func RunHTTPServer(cfg HttpServerConfig) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + t, dumpTranslations := translations.TranslationHelper() + + mcpCfg := MCPServerConfig{ + Version: cfg.Version, + Host: cfg.Host, + Token: cfg.Token, + EnabledToolsets: cfg.EnabledToolsets, + DynamicToolsets: cfg.DynamicToolsets, + ReadOnly: cfg.ReadOnly, + Translator: t, + AppID: cfg.AppID, + AppPrivateKey: cfg.AppPrivateKey, + EnableGitHubAppAuth: cfg.EnableGitHubAppAuth, + InstallationIDHeader: cfg.InstallationIDHeader, + } + + ghServer, err := NewMCPServer(mcpCfg) + if err != nil { + return fmt.Errorf("failed to create MCP server: %w", err) + } + + httpServer := server.NewStreamableHTTPServer(ghServer) + + logrusLogger := logrus.New() + if cfg.LogFilePath != "" { + file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + logrusLogger.SetLevel(logrus.DebugLevel) + logrusLogger.SetOutput(file) + } else { + logrusLogger.SetLevel(logrus.InfoLevel) + } + + if cfg.Address == "" { + cfg.Address = ":8080" + } + if cfg.MCPPath == "" { + cfg.MCPPath = "/mcp" + } + if cfg.InstallationIDHeader == "" { + cfg.InstallationIDHeader = "X-GitHub-Installation-ID" + } + + mux := http.NewServeMux() + var handler http.Handler = httpServer + + // Apply middlewares in the correct order: CORS first, then auth + if cfg.EnableCORS { + handler = corsMiddleware(handler) + } + if cfg.EnableGitHubAppAuth { + handler = authMiddleware(handler, cfg.InstallationIDHeader, logrusLogger) + } + + mux.Handle(cfg.MCPPath, handler) + + srv := &http.Server{ + Addr: cfg.Address, + Handler: mux, + } + + if cfg.ExportTranslations { + dumpTranslations() + } + + errC := make(chan error, 1) + go func() { + logrusLogger.Infof("Starting HTTP server on %s", cfg.Address) + logrusLogger.Infof("MCP endpoint available at http://localhost%s%s", cfg.Address, cfg.MCPPath) + if cfg.EnableGitHubAppAuth { + logrusLogger.Infof("GitHub App authentication enabled with header: %s", cfg.InstallationIDHeader) + } + errC <- srv.ListenAndServe() + }() + + _, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on HTTP at %s\n", cfg.Address) + _, _ = fmt.Fprintf(os.Stderr, "MCP endpoint: http://localhost%s%s\n", cfg.Address, cfg.MCPPath) + if cfg.EnableGitHubAppAuth { + _, _ = fmt.Fprintf(os.Stderr, "GitHub App authentication enabled with header: %s\n", cfg.InstallationIDHeader) + } + + select { + case <-ctx.Done(): + logrusLogger.Infof("shutting down server...") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + logrusLogger.Errorf("error during server shutdown: %v", err) + } + case err := <-errC: + if err != nil && err != http.ErrServerClosed { + return fmt.Errorf("error running server: %w", err) + } + } + + return nil +} + +// corsMiddleware adds CORS headers to allow cross-origin requests +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Host, Origin, Referer, User-Agent") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +// authMiddleware extracts installation IDs from custom headers and adds them to the request context +func authMiddleware(next http.Handler, headerName string, logger *logrus.Logger) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + installationIDStr := r.Header.Get(headerName) + if installationIDStr == "" { + next.ServeHTTP(w, r) + return + } + + installationID, err := strconv.ParseInt(installationIDStr, 10, 64) + if err != nil { + logger.Warnf("Invalid installation ID format in header %s", headerName) + http.Error(w, "Invalid installation ID format", http.StatusBadRequest) + return + } + + if installationID <= 0 { + logger.Warnf("Invalid installation ID value: %d", installationID) + http.Error(w, "Invalid installation ID value", http.StatusBadRequest) + return + } + + ctx := context.WithValue(r.Context(), installationContextKey, installationID) + r = r.WithContext(ctx) + + if logger.GetLevel() == logrus.DebugLevel { + logger.Debugf("Authenticated request with installation ID %d", installationID) + } else { + logger.Debug("Request authenticated with GitHub App installation") + } + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 9a9c73926..ce5a898a7 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "os/signal" + "strconv" "strings" "syscall" @@ -16,10 +17,12 @@ import ( mcplog "github.com/github/github-mcp-server/pkg/log" "github.com/github/github-mcp-server/pkg/translations" gogithub "github.com/google/go-github/v72/github" + "github.com/jferrl/go-githubauth" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" "github.com/sirupsen/logrus" + "golang.org/x/oauth2" ) type MCPServerConfig struct { @@ -45,6 +48,18 @@ type MCPServerConfig struct { // Translator provides translated text for the server tooling Translator translations.TranslationHelperFunc + + // GITHUB APP ID + AppID string + + // GITHUB APP PRIVATE KEY + AppPrivateKey string + + // Whether to enable GitHub App authentication via headers + EnableGitHubAppAuth bool + + // Custom header name to read installation ID from (defaults to "X-GitHub-Installation-ID") + InstallationIDHeader string } func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { @@ -53,22 +68,46 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return nil, fmt.Errorf("failed to parse API host: %w", err) } - // Construct our REST client - restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token) - restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version) - restClient.BaseURL = apiHost.baseRESTURL - restClient.UploadURL = apiHost.uploadURL + privateKey := []byte(cfg.AppPrivateKey) + appID, _ := strconv.ParseInt(cfg.AppID, 10, 64) + + // Set default header name if not provided + if cfg.InstallationIDHeader == "" { + cfg.InstallationIDHeader = "X-GitHub-Installation-ID" + } - // Construct our GraphQL client - // We're using NewEnterpriseClient here unconditionally as opposed to NewClient because we already - // did the necessary API host parsing so that github.com will return the correct URL anyway. - gqlHTTPClient := &http.Client{ - Transport: &bearerAuthTransport{ - transport: http.DefaultTransport, - token: cfg.Token, - }, - } // We're going to wrap the Transport later in beforeInit - gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient) + // Create GitHub App token source if enabled + var appTokenSource oauth2.TokenSource + if cfg.EnableGitHubAppAuth && cfg.AppID != "" && cfg.AppPrivateKey != "" { + var err error + appTokenSource, err = githubauth.NewApplicationTokenSource(appID, privateKey) + if err != nil { + return nil, fmt.Errorf("failed to create GitHub App token source: %w", err) + } + } + + // Only create static clients if not using GitHub App auth + var restClient *gogithub.Client + var gqlClient *githubv4.Client + var gqlHTTPClient *http.Client + if !cfg.EnableGitHubAppAuth || appTokenSource == nil { + restClient = gogithub.NewClient(nil).WithAuthToken(cfg.Token) + restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version) + restClient.BaseURL = apiHost.baseRESTURL + restClient.UploadURL = apiHost.uploadURL + + // Construct our GraphQL client + // We're using NewEnterpriseClient here unconditionally as opposed to NewClient because we already + // did the necessary API host parsing so that github.com will return the correct URL anyway. + // Use static token + gqlHTTPClient = &http.Client{ + Transport: &bearerAuthTransport{ + transport: http.DefaultTransport, + token: cfg.Token, + }, + } + gqlClient = githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient) + } // When a client send an initialize request, update the user agent to include the client info. beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) { @@ -79,11 +118,14 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { message.Params.ClientInfo.Version, ) - restClient.UserAgent = userAgent - - gqlHTTPClient.Transport = &userAgentTransport{ - transport: gqlHTTPClient.Transport, - agent: userAgent, + if restClient != nil { + restClient.UserAgent = userAgent + } + if gqlHTTPClient != nil { + gqlHTTPClient.Transport = &userAgentTransport{ + transport: gqlHTTPClient.Transport, + agent: userAgent, + } } } @@ -104,12 +146,36 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { } } - getClient := func(_ context.Context) (*gogithub.Client, error) { - return restClient, nil // closing over client + // Create dynamic client functions that can handle per-request authentication + getClient := func(ctx context.Context) (*gogithub.Client, error) { + if !cfg.EnableGitHubAppAuth || appTokenSource == nil { + return restClient, nil + } + installationID, ok := ctx.Value(installationContextKey).(int64) + if !ok || installationID <= 0 { + return restClient, nil + } + installationTokenSource := githubauth.NewInstallationTokenSource(installationID, appTokenSource) + oauth2Client := oauth2.NewClient(ctx, installationTokenSource) + restClient = gogithub.NewClient(oauth2Client) + restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version) + restClient.BaseURL = apiHost.baseRESTURL + restClient.UploadURL = apiHost.uploadURL + return restClient, nil } - getGQLClient := func(_ context.Context) (*githubv4.Client, error) { - return gqlClient, nil // closing over client + getGQLClient := func(ctx context.Context) (*githubv4.Client, error) { + if !cfg.EnableGitHubAppAuth || appTokenSource == nil { + return gqlClient, nil + } + installationID, ok := ctx.Value(installationContextKey).(int64) + if !ok || installationID <= 0 { + return gqlClient, nil + } + installationTokenSource := githubauth.NewInstallationTokenSource(installationID, appTokenSource) + oauth2Client := oauth2.NewClient(ctx, installationTokenSource) + gqlClient = githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), oauth2Client) + return gqlClient, nil } // Create default toolsets 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